﻿/****************************** Module Header ******************************\
Module Name:    GetErrorLogEventCommand.cs
Project:        SQL Server Error Log Parsing module

This is the implementation of the Get-ErrorLogEvent Cmdlet, which parses the 
entries in a SQL Server error log file.

This source is subject to the Microsoft Public License.
See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
All other rights reserved.

THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\***************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Text;
using System.Text.RegularExpressions;

namespace SQLServerSleuth.PowerShell.Commands.ParseERRORLOG
{
    /// <summary>
    /// The implementation of the Get-ErrorLogEvent Cmdlet.
    /// </summary>
    [Cmdlet("Get", "ErrorLogEvent")]
    public class GetErrorLogEventCommand : PSCmdlet
    {
        #region Cmdlet parameters

        /// <summary>
        /// The path to the SQL Server error log file.
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
        [ValidateNotNullOrEmpty]
        public string LiteralPath
        {
            get
            {
                return this._literalPath;
            }

            set
            {
                this._literalPath = value.Trim();
            }
        }
        public string _literalPath = "";

        /// <summary>
        /// The path to text file that contains the SQL Server error message 
        /// templates.
        /// </summary>
        [Parameter(Mandatory = true, Position = 1)]
        [ValidateNotNullOrEmpty]
        public string MessageTemplateFilePath
        {
            get
            {
                return this._messageTemplateFilePath;
            }

            set
            {
                this._messageTemplateFilePath = value.Trim();
            }
        }
        private string _messageTemplateFilePath = "";

        /// <summary>
        /// The path to the directory into which the parsing metrics files 
        /// should be written.  If the parsing metrics are unwanted, this 
        /// parameter should not be supplied.
        /// </summary>
        [Parameter(ParameterSetName = "ParsingMetrics", Position = 2)]
        [ValidateNotNullOrEmpty]
        public string ParsingMetricsDirectoryPath
        {
            get
            {
                return this._parsingMetricsDirectoryPath;
            }

            set
            {
                this._parsingMetricsDirectoryPath = value.Trim();
            }
        }
        private string _parsingMetricsDirectoryPath = "";

        /// <summary>
        /// If parsing metrics are going to be saved to disk, this parameter 
        /// controls the format of the output file name.  There are a few 
        /// tokens that can be automatically replaced by run-time meta data.
        /// </summary>
        [Parameter(ParameterSetName = "ParsingMetrics", Position = 3)]
        [ValidateNotNullOrEmpty]
        public string ParsingMetricsFileNameFormat
        {
            get
            {
                return this._parsingMetricsFileNameFormat;
            }

            set
            {
                this._parsingMetricsFileNameFormat = value.Trim();
            }
        }
        private string _parsingMetricsFileNameFormat = "__COMPUTER_NAME__,SQL Server error log parsing metrics,__START_TIME__.xml";

        /// <summary>
        /// Providing this switch indicates that the parsing metrics 
        /// (run-time information on the parsing process) should be written 
        /// to the output pipeline along with the error log entries.
        /// </summary>
        [Parameter(Position = 4)]
        public System.Management.Automation.SwitchParameter EmitParsingMetrics
        {
            get
            {
                return this._emitParsingMetrics;
            }

            set
            {
                this._emitParsingMetrics = value;
            }
        }
        public System.Management.Automation.SwitchParameter _emitParsingMetrics;

        /// <summary>
        /// A HashTable that contains historical counts for the number of 
        /// times that various system messages have been parsed out of 
        /// SQL Server error logs.  It is not necessary to include all 
        /// system messages, only those that should have a historical count 
        /// different from the default of zero.  It is also possible to 
        /// provide negative values, to purposely push infrequent messages 
        /// to the bottle of the list.
        /// </summary>
        [Parameter(Position = 5)]
        [ValidateNotNullOrEmpty]
        public Hashtable HistoricalMatchCounts
        {
            get
            {
                return this._historicalMatchCounts;
            }

            set
            {
                this._historicalMatchCounts = value;
            }
        }
        private Hashtable _historicalMatchCounts = null;

        #endregion Cmdlet parameters

        #region Overridden methods from the System.Management.Automation.PSCmdlet class

        /// <summary>
        /// The BeginProcessing() method, overriden from the PSCmdlet class, 
        /// is called once per PowerShell pipeline.  In the case of the 
        /// Get-ErrorLogEvent Cmdlet, the SQL Server system message 
        /// templates are loaded once from a specifically formatted text 
        /// file.  This means that a given PowerShell pipeline can only 
        /// effectively parse error logs from a single version (build) of 
        /// SQL Server.
        /// It is possible that, in the future, flexibility will be added to 
        /// dynamically identify the correct message template file for each 
        /// error log being processed.  Such a feature would require that 
        /// the loading of the templates be moved to the ProcessRecord() 
        /// method.
        /// </summary>
        protected override void BeginProcessing()
        {
            if (null == this._historicalMatchCounts)
            {
                this._historicalMatchCounts = new Hashtable();
            }

            this.loadMessageTemplates();

            return;
        }

        /// <summary>
        /// The ProcessRecord() method, overriden from the PSCmdlet class, 
        /// is called for each object that is passed through the PowerShell 
        /// pipeline that includes the Get-ErrorLogEvent Cmdlet.  This 
        /// method is responsible for parsing the contents of a single 
        /// SQL Server error log file.
        /// </summary>
        protected override void ProcessRecord()
        {
            //  Write the values of the input parameters to the debug stream.
            this.writeDebugOutput_CmdletParameters();

            //  Reset the match counts for the current SQL Server error log.
            this._messageMatchCounts.ResetCurrentMatchCounts();

            //  Use the default Comparer to sort the collection of 
            //  MessageMatchCount objects in descending order of matches 
            //  (and ascending order of message IDs if there are match count 
            //  ties).
            this._messageMatchCounts.Sort();

            this._startTime = DateTime.Now;

            //  Parse the contents of the given SQL Server error log file.
            this.parseErrorLogFile();

            this._finishTime = DateTime.Now;

            //  If desired (as indicated by the value of the 
            //  -ParsingMetricsDirectoryPath Cmdlet parameter), run-time 
            //  metrics about the parsing operation will be saved to an XML 
            //  document.
            this.saveParsingMetrics();

            return;
        }

        #endregion Overridden methods from the System.Management.Automation.PSCmdlet class

        #region Private class members

        /// <summary>
        /// The ordinal values for the columns that are parsed from the 
        /// message template file.
        /// </summary>
        private enum inputFieldNumbers
        {
            MessageID = 0,
            Severity = 1,
            IsLogged = 2,
            TokenCount = 3,
            MessageTemplateText = 4
        };

        /// <summary>
        /// A constant Regular Expression object that is used to split each 
        /// error log entry into its event time, process, and message text.
        /// </summary>
        private static readonly Regex regEx_LogEntry = new Regex(
            @"(?<EntryTime>\d{4}\-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{2}) +(?<Process>\w+) +(?<Message>.*)",
            (RegexOptions.IgnoreCase | RegexOptions.Singleline)
        );

        /// <summary>
        /// A constant Regular Expression object that is used to split apart 
        /// information on the SQL Server process that generated the event.
        /// </summary>
        private static readonly Regex regEx_ProcessString = new Regex(
            @"spid(?<SessionID>\d+)(?<IsSystemProcess>s)?",
            RegexOptions.IgnoreCase
        );

        /// <summary>
        /// A constant Regular Expression object that is used to validate 
        /// the lines loaded from the message templates file.
        /// </summary>
        private static readonly Regex regEx_MessageTemplateLine = new Regex(
            @"^(?<MessageID>[\d-]+)\t(?<Severity>\d+)\t(?<IsLogged>\d)\t(?<TokenCount>\d+)\t(?<MessageTemplate>.+)$",
            RegexOptions.IgnoreCase
        );

        //  Private member fields for tracking certain run-time metrics 
        //  about the parsing operation.
        private DateTime _earliestEntryTime = DateTime.MaxValue;
        private DateTime _finishTime = DateTime.MinValue;
        private DateTime _latestEntryTime = DateTime.MinValue;
        private MessageMatchCounts _messageMatchCounts = new MessageMatchCounts();
        private DateTime _startTime = DateTime.MaxValue;
        private int _sourceLineCount = 0;

        //  Private member member fields for tracking certain information 
        //  about the system message templates that are being used for 
        //  parsing.
        private int _highestTokenCount = 0;
        private int _messageTemplateCount = 0;
        private Dictionary<int, MessageTemplate> _messageTemplates = new Dictionary<int, MessageTemplate>();

        /// <summary>
        /// Converts an element of a string array to an element of an 
        /// integer array.
        /// </summary>
        /// <param name="integerValue">The value of the array element.</param>
        /// <returns>The integer representation of the array element value.</returns>
        private int convertStringToInteger(string stringValue)
        {
            return int.Parse(stringValue);
        }

        /// <summary>
        /// This method breaks apart the process string from the second 
        /// column of the SQL Server error log.
        /// </summary>
        /// <param name="processString">The process string, as extracted 
        /// from the second column of the SQL Server error log.</param>
        /// <param name="sessionID">The output of the session ID (SPID) or 
        /// zero if there isn't one.</param>
        /// <param name="sessionType">The subsystem from which the session 
        /// originated, if not a user session.</param>
        private void decodeProcessString
        (
            string processString,
            out int sessionID,
            out SQLServerSessionTypes sessionType
        )
        {
            Match processMatch = null;

            //  Initialize the output parameters.
            sessionID = 0;
            sessionType = SQLServerSessionTypes.Unknown;

            //  The conditions are in order of frequency (anecdotally) for 
            //  better performance.
            if ("backup" == processString.ToLowerInvariant())
            {
                sessionType = SQLServerSessionTypes.Backup;
            }
            else if ((processMatch = regEx_ProcessString.Match(processString)).Success)
            {
                sessionID = int.Parse(processMatch.Groups["SessionID"].Value);
                if (processMatch.Groups["IsSystemProcess"].Success)
                {
                    sessionType = SQLServerSessionTypes.System;
                }
                else
                {
                    sessionType = SQLServerSessionTypes.None;
                }
            }
            else if ("server" == processString.ToLowerInvariant())
            {
                sessionType = SQLServerSessionTypes.Server;
            }
            else if ("logon" == processString.ToLowerInvariant())
            {
                sessionType = SQLServerSessionTypes.Logon;
            }

            return;
        }

        /// <summary>
        /// This method generates the name for a parsing metrics XML file 
        /// that is being saved to disk.  Certain tokens will be replaced by 
        /// the appropriate run-time values.
        /// </summary>
        /// <param name="parsingMetricsDirectoryPath">The path to the 
        /// directory into which the parsing metrics file should be saved.</param>
        /// <param name="parsingMetricsFileNameFormat">The base file name 
        /// format string.</param>
        /// <returns>The full path to the parsing metric file.</returns>
        private string getParsingMetricsFilePath
        (
            string parsingMetricsDirectoryPath,
            string parsingMetricsFileNameFormat
        )
        {
            string parsingMetricsFileName = parsingMetricsFileNameFormat;
            string parsingMetricsFilePath = "";

            //  Determine the replacement values for each of the supported 
            //  tokens.
            Hashtable replacementTokens = new Hashtable();
            replacementTokens.Add(
                "__COMPUTER_NAME__",
                System.Environment.MachineName
            );
            replacementTokens.Add(
                "__CURRENT_TIME__",
                DateTime.Now.ToString("yyyyMMddTHHmmssfff")
            );
            replacementTokens.Add(
                "__DOMAIN_NAME__",
                System.Environment.UserDomainName
            );
            replacementTokens.Add(
                "__PID__",
                System.Diagnostics.Process.GetCurrentProcess().Id.ToString("000000")
            );
            replacementTokens.Add(
                "__START_TIME__",
                this._startTime.ToString("yyyyMMddTHHmmssfff")
            );
            replacementTokens.Add(
                "__TICK_COUNT__",
                System.Environment.TickCount.ToString()
            );
            replacementTokens.Add(
                "__USER_NAME__",
                System.Environment.UserName
            );

            //  Iterate through the tokens and replace each one with the 
            //  appropriate run-time value.
            foreach (string replacementToken in replacementTokens.Keys)
            {
                if (parsingMetricsFileName.Contains("__"))
                {
                    parsingMetricsFileName = parsingMetricsFileName.Replace
                    (
                        replacementToken,
                        (string)replacementTokens[replacementToken]
                    );
                }
                else
                {
                    break;
                }
            }

            parsingMetricsFilePath = System.IO.Path.Combine
            (
                parsingMetricsDirectoryPath,
                parsingMetricsFileName
            );

            return parsingMetricsFilePath;
        }

        /// <summary>
        /// This method populates the MessageMatchCounts list based on the 
        /// specified system message template file.
        /// </summary>
        private void loadMessageTemplates()
        {
            int historicalMatchCount = -1;
            byte isLogged = 0;
            string line = "";
            int lineCounter = -1;
            int messageID = -1;
            int messageTemplateCounter = 0;
            string[] messageTemplateFields = null;
            int nextCharacterCode = 0;
            byte tokenCount = 0;

            try
            {
                using (StreamReader messageTemplateStream = File.OpenText(this._messageTemplateFilePath))
                {
                    while (0 <= nextCharacterCode)
                    {
                        //  Read the next line of the message templates file.
                        line = messageTemplateStream.ReadLine();
                        lineCounter++;

                        //  Check the next character in the error log, without 
                        //  advancing the position in the stream.
                        nextCharacterCode = messageTemplateStream.Peek();

                        //  Validate that the expected columns are present 
                        //  in the current line.
                        if (regEx_MessageTemplateLine.IsMatch(line))
                        {
                            messageTemplateFields = line.Split('\t');

                            messageID = int.Parse(messageTemplateFields[(int)inputFieldNumbers.MessageID]);
                            isLogged = byte.Parse(messageTemplateFields[(int)inputFieldNumbers.IsLogged]);
                            tokenCount = byte.Parse(messageTemplateFields[(int)inputFieldNumbers.TokenCount]);

                            //  Instantiate a new MessageTemplate object and 
                            //  add it to the collection.  Each message 
                            //  template pattern is automatically enclosed 
                            //  in anchoring Regular Expression 
                            //  metacharacters:
                            //      "^" (the start-of-line anchor)
                            //      The pattern from the message template 
                            //          file
                            //      "\s*" (zero or more trailing white-space 
                            //          characters
                            //      "$" (the end-of-line anchor)
                            this._messageTemplates.Add
                            (
                                messageID,
                                new MessageTemplate
                                (
                                    messageID,
                                    byte.Parse(messageTemplateFields[(int)inputFieldNumbers.Severity]),
                                    tokenCount,
                                    string.Format
                                    (
                                        "^{0}\\s*$",
                                        messageTemplateFields[(int)inputFieldNumbers.MessageTemplateText]
                                    )
                                )
                            );

                            //  Determine whether the current message ID 
                            //  should be initialized to a non-default value 
                            //  due to information passed to the Cmdlet via 
                            //  the $HistoricalMatchCounts parameter.
                            if (this._historicalMatchCounts.ContainsKey(messageID))
                            {
                                //  Use the value from the corresponding 
                                //  HashTable element.
                                historicalMatchCount = (int)this._historicalMatchCounts[messageID];
                            }
                            else
                            {
                                //  Since the historical match count wasn't 
                                //  specified, the value should be 
                                //  initialized to an arbitrary value.  I 
                                //  haven't determined, conclusively, 
                                //  whether a system message (from the 
                                //  [sys].[messages] catalog view) that has 
                                //  a value of zero in the [is_event_logged] 
                                //  column can show up in the SQL Server 
                                //  error log.  As such, I don't want to 
                                //  filter those messages out entirely.  
                                //  Until then, these messages will be 
                                //  pushed further down the list, to improve 
                                //  performance.  Also, the 
                                //  [is_event_logged] column value can be 
                                //  changed via the [sys].[sp_altermessage] 
                                //  system stored procedure, so this can 
                                //  vary by instance.  Leaving non-logged 
                                //  messages in the system messages file 
                                //  means that the same file can be used 
                                //  effectively with more error log files.
                                if (1 == isLogged)
                                {
                                    historicalMatchCount = 0;
                                }
                                else
                                {
                                    historicalMatchCount = -10;
                                }
                            }

                            //  Add the message to the list of 
                            //  MessageMatchCount objects.
                            this._messageMatchCounts.Add
                            (
                                new MessageMatchCount
                                (
                                    messageID,
                                    0,
                                    historicalMatchCount
                                )
                            );

                            messageTemplateCounter++;

                            if (tokenCount > this._highestTokenCount)
                            {
                                this._highestTokenCount = tokenCount;
                            }
                        }
                        else
                        {
                            //  Verify that the line isn't empty (which is 
                            //  not considered an error condition).
                            if
                            (
                                (!string.IsNullOrEmpty(line))
                                &&
                                (0 < line.Trim().Length)
                            )
                            {
                                //  Create a new error record, populate all 
                                //  relevant properties, and then write it 
                                //  to the error stream.
                                ErrorRecord errorRecord = new ErrorRecord
                                (
                                    new System.IO.FileFormatException
                                    (
                                        string.Format
                                        (
                                            "Line {0} of the \"{1}\" message templates file is not in the expected format.",
                                            lineCounter,
                                            this._messageTemplateFilePath
                                        )
                                    ),
                                    "ParseError",
                                    ErrorCategory.ReadError,
                                    "MessageTemplateFile"
                                );
                                WriteError(errorRecord);
                            }
                        }
                    }

                    //  Verify that at least one message template was 
                    //  successfully loaded.
                    if (1 > messageTemplateCounter)
                    {
                        throw new System.IO.FileFormatException
                        (
                            string.Format
                            (
                                "There were not any valid message templates to load from the \"{0}\" file.",
                                this._messageTemplateFilePath
                            )
                        );
                    }
                }
            }
            catch (Exception exception)
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    exception,
                    "LoadFailure",
                    ErrorCategory.ReadError,
                    "MessageTemplateFile"
                );
                ThrowTerminatingError(errorRecord);
            }

            this._messageTemplateCount = messageTemplateCounter;

            WriteVerbose
            (
                string.Format
                (
                    "{0:N0} message template{1} loaded from the \"{2}\" file.",
                    this._messageTemplateCount,
                    ((this._messageTemplateCount == 1) ? " was" : "s were"),
                    this._messageTemplateFilePath
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "Highest token count = {0}.",
                    this._highestTokenCount
                )
            );

            return;
        }
        
        /// <summary>
        /// This method processes a single entry of the SQL Server error 
        /// log, which is typically a single line of the text file.  Certain 
        /// types of messages may span more than one line, however, with the 
        /// entire message being considered a single entry.
        /// </summary>
        /// <param name="errorLogEntry">The line or lines of the SQL Server 
        /// error log that were read from the error log file.</param>
        /// <param name="sourceLineNumbersString">A comma-delimited list of 
        /// the line numbers that correspond to the entry's location in the 
        /// source SQL Server error log file.</param>
        private void parseErrorLogEntry
        (
            string errorLogEntry,
            string sourceLineNumbersString
        )
        {
            Match errorLogEntryMatch = null;
            string errorMessage = string.Empty;
            DateTime entryTime = DateTime.MinValue;
            string messageText = "";
            int sessionID;
            SQLServerSessionTypes sessionType;
            int messageID = 0;
            byte messageSeverity = 0;
            string[] messageParameters = null;
            int[] sourceLineNumbers = Array.ConvertAll<string, int>
            (
                sourceLineNumbersString.Split(
                    (new char[1] {','}),
                    StringSplitOptions.RemoveEmptyEntries
                ),
                new Converter<string, int>(this.convertStringToInteger)
            );

            errorLogEntryMatch = regEx_LogEntry.Match(errorLogEntry);
            if (errorLogEntryMatch.Success)
            {
                //  Convert the first column, a string that represents the 
                //  time at which the entry occurred, into an actual 
                //  DateTime structure.
                entryTime = DateTime.ParseExact(
                    (errorLogEntryMatch.Groups["EntryTime"].Value),
                    "yyyy-MM-dd HH:mm:ss.ff",
                    System.Globalization.CultureInfo.InvariantCulture
                );
                if (entryTime < this._earliestEntryTime) {
                    this._earliestEntryTime = entryTime;
                }
                if (entryTime > this._latestEntryTime) {
                    this._latestEntryTime = entryTime;
                }

                //  Split apart the components of the process column value.
                this.decodeProcessString
                (
                    (errorLogEntryMatch.Groups["Process"].Value.Trim()),
                    out sessionID,
                    out sessionType
                );

                //  Attempt to identify the message template.  If the 
                //  message type cannot be identified, the output parameters 
                //  will have been initialized to values that indicate that 
                //  message was not successfully parsed.
                messageText = errorLogEntryMatch.Groups["Message"].Value.Trim();
                this.parseSystemMessage
                (
                    messageText,
                    out messageID,
                    out messageSeverity,
                    out messageParameters
                );

                //  Place the parsed details in the output pipeline.
                WriteObject
                (
                    new ErrorLogEvent(
                        entryTime,
                        sessionID,
                        sessionType,
                        messageID,
                        messageSeverity,
                        messageParameters,
                        sourceLineNumbers,
                        messageText,
                        this._literalPath
                    )
                );
            }
            else
            {
                //  Verify that the line isn't empty (which is not 
                //  considered an error condition).
                if
                (
                    (!string.IsNullOrEmpty(errorLogEntry))
                    &&
                    (0 < errorLogEntry.Trim().Length)
                )
                {
                    //  Tailor the error message to the line(s) from which 
                    //  the entry originated.
                    switch (sourceLineNumbers.Length)
                    {
                        case 1:
                            errorMessage = string.Format
                            (
                                "Line {0} of the \"{1}\" SQL Server error log is not in the expected format.",
                                sourceLineNumbers[0],
                                this._literalPath
                            );
                            break;

                        case 2:
                            errorMessage = string.Format
                            (
                                "Lines {0} and {1} of the \"{2}\" SQL Server error log is not in the expected format.",
                                sourceLineNumbers[0],
                                sourceLineNumbers[1],
                                this._literalPath
                            );
                            break;

                        default:
                            if (2 < sourceLineNumbers.Length)
                            {
                                errorMessage = string.Format
                                (
                                    "Lines {0} through {1} of the \"{2}\" SQL Server error log is not in the expected format.",
                                    sourceLineNumbers[0],
                                    sourceLineNumbers[(sourceLineNumbers.Length - 1)],
                                    this._literalPath
                                );
                            }
                            else
                            {
                                errorMessage = string.Format
                                (
                                    "The current line of the \"{0}\" SQL Server error log is not in the expected format.",
                                    this._literalPath
                                );
                            }
                            break;
                    }

                    //  Create a new error record, populate all relevant 
                    //  properties, and then write it to the error stream.
                    ErrorRecord errorRecord = new ErrorRecord
                    (
                        new System.IO.FileFormatException (errorMessage),
                        "ParseError",
                        ErrorCategory.ReadError,
                        "SQLServerErrorLogFile"
                    );
                    WriteError(errorRecord);
                }
            }

            return;
        }

        /// <summary>
        /// This method iterates through the lines of the SQL Server error 
        /// log and passes each complete entry (single or multi-line) on to 
        /// be processed.
        /// </summary>
        private void parseErrorLogFile()
        {
            StreamReader errorLogStream = null;
            bool isEntryComplete = false;
            string line = "";
            int nextCharacterCode = 0;
            StringBuilder rawErrorLogEntry = new StringBuilder();
            int sourceLineCounter = 0;
            StringBuilder sourceLineNumbers = new StringBuilder();

            try
            {
                errorLogStream = File.OpenText(this._literalPath);

                while (0 <= nextCharacterCode)
                {
                    //  Read the next line of the SQL Server error log.
                    line =  errorLogStream.ReadLine();
                    sourceLineNumbers.AppendFormat
                    (
                        ",{0}",
                        ++sourceLineCounter
                    );

                    //  Determine whether to append a trailing carriage 
                    //  return and line feed (which will only be done for 
                    //  multi-line messages, other than the final line).
                    if (isEntryComplete) {
                        rawErrorLogEntry.Append(line);
                    }
                    else {
                        rawErrorLogEntry.AppendFormat(
                            "\r\n{0}",
                            line
                        );
                    }

                    //  Check the next character in the error log, without 
                    //  advancing the position in the stream.
                    nextCharacterCode = errorLogStream.Peek();

                    //  Determine whether the following line in the error log 
                    //  is a continuation of the current one.  This is the 
                    //  case when the next character code is not -1 
                    //  (indicating that the end of the error log has been 
                    //  reached) and isn't "1" or "2" (the first character in 
                    //  the date column of an error log entry--"19??" or 
                    //  "20??").
                    if
                    (
                        (-1 == nextCharacterCode)
                        ||
                        (0x31 == nextCharacterCode)
                        ||
                        (0x32 == nextCharacterCode)
                    )
                    {
                        isEntryComplete = true;

                        //  Hand the complete entry off for processing.
                        parseErrorLogEntry
                        (
                            rawErrorLogEntry.ToString(),
                            sourceLineNumbers.ToString()
                        );
                        
                        //  Reset both StringBuilder objects.
                        rawErrorLogEntry.Length = 0;
                        sourceLineNumbers.Length = 0;
                    }
                    else
                    {
                        isEntryComplete = false;
                    }
                }

                this._sourceLineCount = sourceLineCounter;
            }
            catch (Exception exception)
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    exception,
                    "ParseFailure",
                    ErrorCategory.ReadError,
                    "SQLServerErrorLogFile"
                );
                ThrowTerminatingError(errorRecord);
            }
            finally
            {
                if (null != errorLogStream) {
                    errorLogStream.Dispose();
                }
            }

            return;
        }

        /// <summary>
        /// This method performs the bulk of the work in parsing the 
        /// SQL Server error log.  For each entry, it iterates through the 
        /// provided message templates searching for a match.  If one is 
        /// located, it returns the corresponding message ID, the typical 
        /// severity of the message type, and an array of the parameters 
        /// that are specific to the current instance of the system 
        /// message.  If a match isn't found, the output variables will be 
        /// returned in their initialized state, allowing the caller to 
        /// determine that the parsing failed.
        /// </summary>
        /// <param name="messageText">The text of the current entry in the 
        /// SQL Server error log.</param>
        /// <param name="messageID">The ID of the system message that 
        /// corresponds to the entry.</param>
        /// <param name="messageSeverity">The typical severity of an error 
        /// of the given type.</param>
        /// <param name="messageParameters">An array with zero or more 
        /// parameters to the message template.</param>
        private void parseSystemMessage
        (
            string messageText,
            out int messageID,
            out byte messageSeverity,
            out string[] messageParameters
        )
        {
            bool isTemplateMatch = false;
            Match messageTemplateMatch = null;
            int parameterCounter = 0;
            int potentialMessageID = -1;
            int messageTemplateCounter = 0;

            //  Initialize the output parameters.
            messageID = 0;
            messageSeverity = 0;
            messageParameters = new string[0];

            //  Iterate through the message templates, searching for a 
            //  match against the current error log entry.
            while
            (
                (messageTemplateCounter < this._messageTemplateCount)
                &&
                (!isTemplateMatch)
            )
            {
                //  Get the next message ID from the list of message 
                //  matches.  This allows the message templates to be 
                //  sorted by the popularity of the message template 
                //  rather by the numeric value of the message ID value 
                //  (which is basically arbitrary).
                potentialMessageID = this._messageMatchCounts[messageTemplateCounter].MessageID;

                messageTemplateMatch = this._messageTemplates[potentialMessageID].RegularExpression.Match(messageText);
                if (messageTemplateMatch.Success)
                {
                    isTemplateMatch = true;

                    //  Update the output parameters with the details of 
                    //  the match.
                    messageID = potentialMessageID;
                    messageSeverity = this._messageTemplates[messageID].Severity;
                    messageParameters = new string[this._messageTemplates[messageID].TokenCount];
                    for (parameterCounter = 1; parameterCounter < messageTemplateMatch.Groups.Count; parameterCounter++)
                    {
                        messageParameters[(parameterCounter - 1)] = messageTemplateMatch.Groups[parameterCounter].Value;
                    }

                    //  Update the statistics on the number of times 
                    //  that this message template has been matched, 
                    //  both for the current error log file and overall.
                    //  NOTE: By incrementing both values, each match in the 
                    //  current error log is essentially getting counted 
                    //  twice (since the MessageMatchCounts.Sort() method 
                    //  adds the two values together).  This is intentional, 
                    //  as it slightly weights message templates that have 
                    //  been found in the current error log.
                    this._messageMatchCounts[messageTemplateCounter].CurrentLogFileMatchCount++;
                    this._messageMatchCounts[messageTemplateCounter].HistoricalMatchCount++;
                }
                messageTemplateCounter++;
            }

            return;
        }

        /// <summary>
        /// This method saves details of the parsing operation as an XML file.
        /// </summary>
        private void saveParsingMetrics()
        {
            string parsingMetricsFilePath = "";

            ParsingMetrics parsingMetrics = new ParsingMetrics
            (
                this._literalPath,
                this._messageTemplateFilePath,
                this._messageTemplateCount,
                this._highestTokenCount,
                this._sourceLineCount,
                this._startTime,
                this._finishTime,
                this._messageMatchCounts,
                this._earliestEntryTime,
                this._latestEntryTime
            );

            //  Place the parsing metrics object in the output pipeline, if 
            //  requested.
            if (this._emitParsingMetrics.IsPresent)
            {
                WriteObject (parsingMetrics);
            }

            //  Determine whether the parsing metrics should be saved to 
            //  disk.
            if
            (
                (string.IsNullOrEmpty(this._parsingMetricsDirectoryPath))
                ||
                (string.IsNullOrEmpty(this._parsingMetricsFileNameFormat))
            )
            {
                WriteVerbose
                (
                    "The parsing metrics are not being saved due to the values of the -ParsingMetricsDirectoryPath and/or -ParsingMetricsFileNameFormat parameters."
                );
            }
            else
            {
                //  Assemble the name of the output file.
                parsingMetricsFilePath = this.getParsingMetricsFilePath
                (
                    this._parsingMetricsDirectoryPath,
                    this._parsingMetricsFileNameFormat
                );

                //  Verify that the output file will not be overwriting 
                //  another file.
                if (File.Exists(parsingMetricsFilePath))
                {
                    WriteVerbose
                    (
                        string.Format
                        (
                            "The parsing metrics are not being saved because the \"{0}\" file already exists.",
                            parsingMetricsFilePath
                        )
                    );
                }
                else
                {
                    System.Xml.XmlDocument parsingMetricsXMLDocument = parsingMetrics.ToXML();
                    parsingMetricsXMLDocument.Save(parsingMetricsFilePath);
                    WriteVerbose
                    (
                        string.Format
                        (
                            "The parsing metrics have been saved to the \"{0}\" file.",
                            parsingMetricsFilePath
                        )
                    );
                }
            }

            return;
        }

        /// <summary>
        /// Write the values of all relevant Cmdlet parameters and arguments 
        /// to the debug stream.
        /// </summary>
        private void writeDebugOutput_CmdletParameters()
        {
            WriteDebug
            (
                string.Format
                (
                    "Parameter set = {0}",
                    this.ParameterSetName
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$LiteralPath = \"{0}\"",
                    this._literalPath
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$MessageTemplateFilePath = \"{0}\"",
                    this._messageTemplateFilePath
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$ParsingMetricsDirectoryPath = \"{0}\"",
                    this._parsingMetricsDirectoryPath
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$ParsingMetricsFileNameFormat = \"{0}\"",
                    this._parsingMetricsFileNameFormat
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$HistoricalMatchCounts.Count = {0}",
                    this._historicalMatchCounts.Count
                )
            );

            return;
        }
        
        #endregion Private class members
    }
}
